import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# у нас есть датафрейм, в котором содержится информация о фотографиях и возрасте людей на них
labels = pd.read_csv('/datasets/faces/labels.csv')
# выведем датафрейм, чтобы лучше представлять структуру данных и понимать, как настраивать наш загрузчик данных
labels.head()
| file_name | real_age | |
|---|---|---|
| 0 | 000000.jpg | 4 |
| 1 | 000001.jpg | 18 |
| 2 | 000002.jpg | 80 |
| 3 | 000003.jpg | 50 |
| 4 | 000004.jpg | 17 |
Первый столбец отвечает за имя файла, а второй - за целевой признак.
labels.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 7591 entries, 0 to 7590 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 file_name 7591 non-null object 1 real_age 7591 non-null int64 dtypes: int64(1), object(1) memory usage: 118.7+ KB
# посмотрим какое распределение имеет возраст
# Рисуем гистограмму с плотностью вероятностей, потому что у нас неполный датасет
plt.figure(figsize=(15, 4), dpi=180)
sns.distplot(labels.real_age)
plt.xlabel('Возраст')
plt.ylabel('Вероятность')
plt.suptitle('Распределение возрастов людей в датасете')
plt.show()
plt.close()
/opt/conda/lib/python3.9/site-packages/seaborn/distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning)
Посмотрим на фотографии из обучающей выборки.
train_datagen = ImageDataGenerator(rescale=1./255)
train_gen_flow = train_datagen.flow_from_dataframe(
dataframe=labels,
directory='/datasets/faces/final_files/',
x_col='file_name',
y_col='real_age',
target_size=(224, 224),
batch_size=32,
class_mode='raw'
)
Found 7591 validated image filenames.
# возьмём из итератора один батч
images = next(train_gen_flow)
# посмотрим на размер батча с изображениями
images[0].shape
(32, 224, 224, 3)
Видим 32 картинки размером 224*224 пикселя. Каждый пиксель состоит из 3 значений: красный, зелёный, синий.
Используем Matplotlib для отображения фотографий.
# напишем функцию, которая убирает все оси с изображения
def remove_ticks():
plt.xticks([])
plt.yticks([])
# рисуем фотографии вместе с возрастом
plt.figure(figsize=(14, 7.5), dpi=200)
for (i, img), label in zip(enumerate(images[0][:21]),
images[1]):
plt.subplot(3, 7, i + 1)
plt.imshow(img)
remove_ticks()
plt.xlabel(f'Возраст: {label}')
plt.suptitle('Примеры фотографий из набора данных')
plt.tight_layout(pad=1)
plt.show()
plt.close()
Фотографии самые разнообразные:
Мы не будем обучать модель здесь, а просто скопируем код, который задаёт её параметры. Сама модель будет обучена на GPU отдельно.
В качестве модели мы испытаем архитектуру ResNet50, предобученную на базе ImageNet. Верхушкой нашей модели будет полносвязный слой с одним нейроном: для предсказания единственного значения этого достаточно. Мы выставили количество эпох, равное 26: опытным путём мы установили, что примерно такого значения достаточно, чтобы получить отличное качество при валидации. В качестве функции потерь зададим MSE. Мы попробовали разные значения скорости обучения и выяснили, что значение 0.0001 вполне нам подходит, поскольку при длительном обучении и большем learning rate функция потерь начинает "застревать" и колебаться между похожими значениями - а значит, ходить кругами вокруг минимума, но не попадать в него.
Перенесите сюда код обучения модели и её результат вывода на экран.
(Код в этом разделе запускается в отдельном GPU-тренажёре, поэтому оформлен не как ячейка с кодом, а как код в текстовой ячейке)
Код модели
# импортируем необходимые объекты и модули
from tensorflow.keras.applications.resnet import ResNet50
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.layers import GlobalAveragePooling2D, Dense
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
import numpy as np
import pandas as pd
import tensorflow as tf
# создание загрузчика для обучающей выборки с отражением фотографий по горизонтали
def load_train(path):
labels = pd.read_csv(path + 'labels.csv')
datagen = ImageDataGenerator(validation_split=0.25, horizontal_flip=True, rescale=1./255)
train_gen_flow = datagen.flow_from_dataframe(
dataframe=labels,
directory=path + 'final_files/',
x_col='file_name',
y_col='real_age',
target_size=(224, 224),
batch_size=16,
class_mode='raw',
subset='training',
seed=12345)
return train_gen_flow
# создание загрузчика для тестовой выборки
def load_test(path):
labels = pd.read_csv(path + 'labels.csv')
datagen = ImageDataGenerator(validation_split=0.25, rescale=1./255)
test_gen_flow = datagen.flow_from_dataframe(
dataframe=labels,
directory=path + 'final_files/',
x_col='file_name',
y_col='real_age',
target_size=(224, 224),
batch_size=16,
class_mode='raw',
subset='validation',
seed=12345)
return test_gen_flow
# создание модели
def create_model(input_shape):
backbone = ResNet50(input_shape=input_shape,
weights='imagenet',
include_top=False)
model = Sequential()
model.add(backbone)
model.add(GlobalAveragePooling2D())
model.add(Dense(1, activation='relu'))
optimizer = Adam(lr=0.0001)
model.compile(optimizer=optimizer, loss='mean_squared_error',
metrics=['mae'])
return model
# параметры обучения
def train_model(model, train_data, test_data, batch_size=None, epochs=10,
steps_per_epoch=None, validation_steps=None):
if steps_per_epoch is None:
steps_per_epoch = len(train_data)
if validation_steps is None:
validation_steps = len(test_data)
model.fit(train_data,
validation_data=test_data,
batch_size=batch_size,
epochs=epochs,
steps_per_epoch=steps_per_epoch,
validation_steps=validation_steps,
verbose=2)
return model
Вывод консоли
Using TensorFlow backend.
Found 5694 validated image filenames.
Found 1897 validated image filenames.
<class 'tensorflow.python.keras.engine.sequential.Sequential'>
Train for 356 steps, validate for 119 steps
Epoch 1/10
2022-06-23 16:42:25.789817: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library libcublas.so.10
2022-06-23 16:42:26.078268: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library libcudnn.so.7
356/356 - 48s - loss: 218.5762 - mae: 10.5990 - val_loss: 472.2291 - val_mae: 16.6123
Epoch 2/10
356/356 - 39s - loss: 84.7291 - mae: 6.9770 - val_loss: 130.5261 - val_mae: 8.7699
Epoch 3/10
356/356 - 39s - loss: 59.5567 - mae: 5.9253 - val_loss: 72.2174 - val_mae: 6.3433
Epoch 4/10
356/356 - 39s - loss: 41.5576 - mae: 4.9294 - val_loss: 85.0605 - val_mae: 6.9138
Epoch 5/10
356/356 - 38s - loss: 31.4504 - mae: 4.2790 - val_loss: 65.8210 - val_mae: 6.1303
Epoch 6/10
356/356 - 39s - loss: 24.9407 - mae: 3.7870 - val_loss: 73.1331 - val_mae: 6.4077
Epoch 7/10
356/356 - 39s - loss: 19.4462 - mae: 3.3749 - val_loss: 66.7495 - val_mae: 6.2473
Epoch 8/10
356/356 - 39s - loss: 15.7245 - mae: 3.0432 - val_loss: 69.7703 - val_mae: 6.2007
Epoch 9/10
356/356 - 39s - loss: 14.5598 - mae: 2.9097 - val_loss: 65.5894 - val_mae: 6.0967
Epoch 10/10
356/356 - 39s - loss: 13.1682 - mae: 2.7769 - val_loss: 66.0007 - val_mae: 6.2167
WARNING:tensorflow:sample_weight modes were coerced from
...
to
['...']
119/119 - 9s - loss: 66.0007 - mae: 6.2167
Test MAE: 6.2167
Вывод
Модель сверточной нейронной сети построена на ResNet50.
За 10 эпох обучения удалось уменьшить MAE с 10.6 до 2.8 на тренировочной выборке. Дальнейшее обучение, возможно, улучшит результат.
*Средняя ошибка модели равна 6. Что это может значить?
Если мы предложим модели назвать возраст ребёнка на фотографии, то она вряд ли перепутает его с пожилым человеком. Сложно сказать, на людях каких возрастов модель ошибается больше всего. У нас нет возможности посмотреть на предсказания в силу реализации обучения, но если бы они у нас имелись, то мы могли бы категоризовать фотографии по возрасту и найти ошибку для каждой категории. В работу модели могут вносить случайность сами люди с фотографий: некоторые люди выглядят значительно старше своего возраста, другие - наоборот. Опять же, перед нами не стоит цель назвать точный возраст конкретного человека. Модель скорее оценивает фотографию по тем признакам, которые ей удалось усвоить, и говорит "человек с такой-то внешностью выглядит на такой-то возраст". Возможно, что в этом смысле модель ведёт себя подобно людям и ошибается в тех же местах, где ошибся бы человек.
*Будет ли от модели польза для бизнеса?
Если мы имеем ошибку в 6 лет, то рискуем не отличить взрослого от ребёнка в случае с продажей алкоголя. Паспорт всё так же придётся спрашивать. С рекомендацией товаров всё лучше: потребности людей не меняются кардинально раз в год, и у нас будет возможность довольно точно определять крупные возрастные категории - детей, подростков, молодых, зрелых, пенсионеров и изучать их потребности в автоматическом режиме. Допустим, если у нас есть предсказанный возраст человека и его чек, то мы можем совместить данные о транзакции с возрастом и сделать предположение о том, что тот или иной товар интересен аудитории.